/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package xjson.engine;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import xjson.*;


/**
 *
 * @author olawale
 */
public class Dal {
    private static Dal dal;

    public Dal(){
        store = Store.getInstance();
    }

    public static Dal getInstance(){
        if(dal == null){
            dal = new Dal();
        }
        return dal;
    }
    private Store store;
    private static final String jkey = "jkey";
    private Date callTime;

    public <T> void create(T entity){
        check(entity);
        try {
            Date date = new Date();
            callTime(date);
            JSONObject obj = new JSONObject();
            createObject(entity, obj);
            JSONArray array = Store.getInstance().getFile(entity.getClass());
            Object [] keys = getAllMap(entity.getClass()).keySet().toArray();
            Arrays.sort(keys);
            int key = (keys.length > 0) ? Integer.parseInt(keys[keys.length - 1].toString()) + 1 : 1;
            obj.put(jkey, key);
            setJKeyValue(entity, key);
            array.put(obj);
            addReferences(entity);
            System.out.println("save create " );
            if(date == callTime){
                System.out.println("this is the calling method " );
                date = null;
                store.saveFile(array, entity.getClass());
            }
        } catch (Exception ex) {
            Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    

    public <T> void merge(T entity){
        check(entity);
        try {
            Date date = new Date();
            callTime(date);
            Map<Integer, JSONObject> map = store.getAllMap(entity.getClass());
            int key = getJKeyValue(entity);
            if(map.remove(key) == null)
                return;
            JSONObject obj = new JSONObject();
            createObject(entity, obj);
            obj.put(jkey, key);
            map.put(key, obj);
            System.out.println("save merge " );
            if(date == callTime){
                System.out.println("this is the calling method " );
                date = null;
                store.saveFile(new JSONArray(map.values()), entity.getClass());
            }
//            store.saveFile(new JSONArray(map.values()), entity.getClass());
        } catch (Exception ex) {
            Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public <T> void delete(T entity){
        check(entity);
        try {
            Date date = new Date();
            callTime(date);
            Map<Integer, JSONObject> map = store.getAllMap(entity.getClass());
            int key = getJKeyValue(entity);
            if(map.remove(key) == null)
                return;
            System.out.println("save delete " );
            if(date == callTime){
                System.out.println("this is the calling method " );
                date = null;
                store.saveFile(new JSONArray(map.values()), entity.getClass());
            }
        } catch (Exception ex) {
            Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public <T> T get(Class cl, Integer key){
        Map<Integer, T> map = getAllMap(cl);
        return map.get(key);
    }

    public <T> List<T> getAll(Class cl){
        JSONArray array = store.getFile(cl);
        ArrayList<T> list = new ArrayList();
        for(int i = 0; i < array.length(); i++){
            try {
                list.add((T)getObject(array.getJSONObject(i), cl.newInstance()));
            } catch (Exception ex) {
                Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return list;
    }

    public <T> boolean exists(T entity){
        int key = getJKeyValue(entity);
        return getAllMap(entity.getClass()).containsKey(key);
    }

    public <T> boolean check(T entity){
        String classname = entity.getClass().getName();
        String msg;
        if(!entity.getClass().isAnnotationPresent(JEntity.class)){
            msg = classname + " is not a valid JEntity. Does not contain the " +
                    "JEntity annotation";
            throw new IllegalArgumentException(msg);
        }
        Field [] fields = entity.getClass().getDeclaredFields();
        int jkeycount = 0;
        for(Field f : fields){
            if(f.isAnnotationPresent(JKey.class)){
                ++jkeycount;
                if(jkeycount > 1){
                    msg = classname + " must have only have one JKey annotation";
                    throw new IllegalArgumentException(msg);
                }
                Class type = f.getType();
                if(!Modifier.isFinal(f.getModifiers()) || (type != int.class && type != Integer.class)){
                    msg = "fields that have JKey annotation must be integer and declared final";
                    throw new IllegalArgumentException(msg);
                }
                if(f.isAnnotationPresent(JNode.class)){
                    msg = "fields cannot have both JKey and JNode annontation";
                    throw new IllegalArgumentException(msg);
                }
                if(f.isAnnotationPresent(JReference.class)){
                    msg = "fields cannot have both JKey and JReference annontation";
                    throw new IllegalArgumentException(msg);
                }
            }else if(f.isAnnotationPresent(JNode.class)){
                JNode jnode = f.getAnnotation(JNode.class);
                if(f.getName().equalsIgnoreCase(jkey) || jnode.name().equalsIgnoreCase(jkey)){
                    msg = jkey + " cannot be used as an attribute name, it is reserved";
                    throw new IllegalArgumentException(msg);
                }
                if(f.isAnnotationPresent(JReference.class)){
                    msg = f.getName() + " fields cannot have both JNode and JReference annontation";
                    throw new IllegalArgumentException(msg);
                }
            }else if(f.isAnnotationPresent(JReference.class)){
                JReference jref = f.getAnnotation(JReference.class);
                Class target = f.getAnnotation(JReference.class).target();
                if(entity.getClass().getAnnotation(JEntity.class).subEntity()){
                    msg = "a sub entity cannot hava a JReference node";
                    throw new IllegalArgumentException(msg);
                }
                if(f.getName().equalsIgnoreCase(jkey) || jref.name().equalsIgnoreCase(jkey)){
                    msg = jkey + " cannot be used as an attribute name, it is reserved";
                    throw new IllegalArgumentException(msg);
                }
                if(!target.isAnnotationPresent(JEntity.class)){
                    msg = "the target class for field " + f.getName() + " must be marked as a JEntity";
                    throw new IllegalArgumentException(msg);
                }
                if(target == entity.getClass()){
                    msg = "the target class for field " + f.getName() + " cannot reference the class";
                    throw new IllegalArgumentException(msg);
                }
            }
            if(jkeycount > 0 && entity.getClass().getAnnotation(JEntity.class).subEntity()){
                msg = classname + "JEntity marked as sub entity must not have a JKey annotation ";
                    throw new IllegalArgumentException(msg);
            }
        }
        if(jkeycount == 0 && !entity.getClass().getAnnotation(JEntity.class).subEntity()){
            msg = classname + " must have one JKey annotation except it is a subentity";
                throw new IllegalArgumentException(msg);
        }
        return true;
    }

    public <T> Map<Integer, T> getAllMap(Class cl){
        List<T> list = getAll(cl);
        HashMap<Integer, T> map = new HashMap();
        for(T t : list){
            int key = getJKeyValue(t);
            map.put(key, t);
        }
        return map;
    }

    public <T, R> R getReferenceBy(T entity, Class refClass){
        int skey = getJKeyValue(entity);
        for(Field f : refClass.getDeclaredFields()){
            JReference jref = f.getAnnotation(JReference.class);
            if(jref != null && jref.target() == entity.getClass()){
                String refKey = getRefKey(refClass, jref.target());
                System.out.println("refkey: " + refKey);
                HashMap<Integer, JSONObject> map = store.getAllMap(entity.getClass());
                int tkey = map.get(skey).optInt(refKey);
                R obj = this.<R>get(refClass, tkey);
                return obj;
            }
        }
        return null;
    }

    private void callTime(Date date){
        System.out.println("calling call time " );
        if(callTime == null){
            System.out.println("setting call time " );
            callTime = date;
        }
    }

    private <T> int getJKeyValue(T entity){
        Field [] fields = entity.getClass().getDeclaredFields();
        for(Field f : fields){
            if(f.isAnnotationPresent(JKey.class)){
                try {
                    f.setAccessible(true);
                    int key = f.getInt(entity);
                    return key;
                } catch (IllegalArgumentException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                } catch (IllegalAccessException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        return -1;
    }

    private <T> void setJKeyValue(T entity, int value){
        Field [] fields = entity.getClass().getDeclaredFields();
        for(Field f : fields){
            if(f.isAnnotationPresent(JKey.class)){
                try {
                    f.setAccessible(true);
                    f.setInt(entity, value);
                } catch (IllegalArgumentException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                } catch (IllegalAccessException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }

    private String getRefKey(Class source, Class target){
        String key = "jref." + source.getSimpleName() + "." + target.getSimpleName();
        return key;
    }

    private <T> void addReferences(T entity){
        Field [] fields = entity.getClass().getDeclaredFields();
        int skey = getJKeyValue(entity);
        for(Field f : fields){
            f.setAccessible(true);
            if(f.isAnnotationPresent(JReference.class)){
                try {
                    Class target = f.getType();
                    Object value = f.get(entity);
                    JReference jref = f.getAnnotation(JReference.class);
                    JEntity jentity = (JEntity) jref.target().getAnnotation(JEntity.class);
                    if(jentity.subEntity()) continue;
                    HashMap<Integer, JSONObject> map = store.getAllMap(jref.target());
                    String refKey = getRefKey(entity.getClass(), jref.target());
                    if(target.isArray()){
                        for(int i = 0; i < Array.getLength(value); i++){
                            int tkey = getJKeyValue(Array.get(value, i));
                            JSONObject jo = map.get(tkey);
                            if(jo != null) jo.put(refKey, skey);
                        }
                    }else if(target == List.class || List.class.isAssignableFrom(target)){
                        List list = List.class.cast(value);
                        for(Object o : list){
                            int tkey = getJKeyValue(o);
                            JSONObject jo = map.get(tkey);
                            if(jo != null) jo.put(refKey, skey);
                        }
                    }else if(target.isAnnotationPresent(JEntity.class)){
                        int tkey = getJKeyValue(value);
                        JSONObject jo = map.get(tkey);
                        if(jo != null) jo.put(refKey, skey);
                    }
                    store.saveFile(new JSONArray(map.values()), jref.target());
                } catch (JSONException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                } catch (IllegalArgumentException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                } catch (IllegalAccessException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }

    private <T> void setReferences(T entity){
        Field [] fields = entity.getClass().getDeclaredFields();
        for(Field f : fields){
            f.setAccessible(true);
            Class target = f.getType();
            if(f.isAnnotationPresent(JReference.class)){
                try {
                    JReference jref = f.getAnnotation(JReference.class);
                    JEntity jentity = (JEntity) jref.target().getAnnotation(JEntity.class);
                    if(jentity.subEntity()) continue;
                    int skey = getJKeyValue(entity);
                    String refKey = getRefKey(entity.getClass(), jref.target());
                    HashMap<Integer, JSONObject> map = store.getAllMap(jref.target());
                    ArrayList tmpList = new ArrayList();
                    for(JSONObject obj : map.values()){
                        if(obj.optInt(refKey, -1) == skey){
                            int tkey = obj.getInt(jkey);
                            Object o = get(jref.target(), tkey);
                            tmpList.add(o);
                        }
                    }
                    if(tmpList.size() == 0) return;
                    if(target.isArray()){
                        Object array = Array.newInstance(jref.target(), tmpList.size());
                        for(int i = 0; i < tmpList.size(); i++){
                            Array.set(array, i, tmpList.get(i));
                        }
                        f.set(entity, array);
                    }else if(target == List.class || List.class.isAssignableFrom(target)){
                        List list = (List) target.newInstance();
                        for(int i = 0; i < tmpList.size(); i++){
                            list.add(tmpList.get(i));
                        }
                        f.set(entity, list);
                    }else if(target.isAnnotationPresent(JEntity.class)){
                        f.set(entity, tmpList.get(0));
                    }

                } catch (InstantiationException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                } catch (IllegalAccessException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                } catch (JSONException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                } catch (IllegalArgumentException ex) {
                    Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }

    private <T> JSONObject createObject(T entity, JSONObject jobject) throws Exception{
        Field [] fields = entity.getClass().getDeclaredFields();
        for(Field f : fields){
            try {
                f.setAccessible(true);
                if(f.isAnnotationPresent(JNode.class)){
                    JNode jnode = f.getAnnotation(JNode.class);
                    String name = (jnode.name().isEmpty()) ? f.getName() : jnode.name();
                    if(f.get(entity) == null){
                        jobject.put(name, JSONObject.NULL);
                    }else if(f.getType() == boolean.class || f.getType() == Boolean.class){
                        jobject.put(name, f.getBoolean(entity));
                    }else if(f.getType() == Date.class){
                        jobject.put(name, (Date)f.get(entity));
                    }else if(f.getType() == double.class || f.getType() == Double.class){
                        jobject.put(name, f.getDouble(entity));
                    }else if(f.getType() == float.class || f.getType() == Float.class){
                        jobject.put(name, f.getFloat(entity));
                    }else if(f.getType() == int.class || f.getType() == Integer.class){
                        jobject.put(name, f.getInt(entity));
                    }else if(f.getType() == long.class || f.getType() == Long.class){
                        jobject.put(name, f.getLong(entity));
                    }else if(f.getType() == String.class){
                        jobject.put(name, (String)f.get(entity));
                    }
                }else if(f.isAnnotationPresent(JReference.class)){
                    JReference jref = f.getAnnotation(JReference.class);
                    String name = (jref.name().isEmpty()) ? f.getName() : jref.name();
                    Class type = f.getType();
                    Object value = f.get(entity);
                    JEntity jentity = (JEntity) jref.target().getAnnotation(JEntity.class);
                    if(jentity.subEntity()){
                        if(value == null){
                            jobject.put(name, JSONObject.NULL);
                        }else if(type.isArray()){
                        JSONArray array = new JSONArray();
                        int size = Array.getLength(value);
                        for(int i = 0; i < size; i++){
                            Object e = Array.get(value, i);
                            JSONObject jo = createObject(e, new JSONObject());
                            array.put(i, jo);
                        }
                        jobject.put(name, array);
                        }else if(type == List.class || List.class.isAssignableFrom(type)){
                            JSONArray array = new JSONArray();
                            List list = List.class.cast(value);
                            int size = list.size();
                            for(int i = 0; i < size; i++){
                                Object e = list.get(i);
                                JSONObject jo = createObject(e, new JSONObject());
                                array.put(i, jo);
                            }
                            jobject.put(name, array);
                        }else if(type.isAnnotationPresent(JEntity.class)){
                            JSONObject obj = new JSONObject();
                            obj = createObject(value, obj);
                            jobject.put(name, obj);
                        }
                    }else{
                        if(value == null){
                            continue;
                        }else if(type.isArray()){
                            int size = Array.getLength(value);
                            for(int i = 0; i < size; i++){
                                Object o = Array.get(value, i);
                                if(exists(o)) merge(o);
                                else create(o);
                            }
                        }else if(type == List.class || List.class.isAssignableFrom(type)){
                            List list = List.class.cast(value);
                            for(Object o : list){
                                if(exists(o)) merge(o);
                                else create(o);
                            }
                        }else if(type.isAnnotationPresent(JEntity.class)){
                            if(exists(value)) merge(value);
                            else create(value);
                        }
                    }
                }
            } catch (Exception ex) {
                throw ex;
            }
        }
        return jobject;
    }
    
    private <T> T getObject(JSONObject jobject, T entity) throws JSONException{
        try{
            Field [] fields = entity.getClass().getDeclaredFields();
            for(Field f : fields){
                f.setAccessible(true);
                if(f.isAnnotationPresent(JNode.class)){
                    JNode jnode = f.getAnnotation(JNode.class);
                    String name = (jnode.name().isEmpty()) ? f.getName() : jnode.name();
                    if(jobject.get(name) == JSONObject.NULL){
                        f.set(entity, null);
                    }else if(f.getType() == boolean.class || f.getType() == Boolean.class){
                        f.setBoolean(entity, jobject.getBoolean(name));
                    }else if(f.getType() == Date.class){
                        f.set(entity, jobject.getDate(name));
                    }else if(f.getType() == double.class || f.getType() == Double.class){
                        f.setDouble(entity, jobject.getDouble(name));
                    }else if(f.getType() == float.class || f.getType() == Float.class){
                        f.setFloat(entity, jobject.getFloat(name));
                    }else if(f.getType() == int.class || f.getType() == Integer.class){
                        f.setInt(entity, jobject.getInt(name));
                    }else if(f.getType() == long.class || f.getType() == Long.class){
                        f.setLong(entity, jobject.getLong(name));
                    }else if(f.getType() == String.class){
                        f.set(entity, jobject.get(name));
                    }
                }else if(f.isAnnotationPresent(JReference.class)){
                    JReference jref = f.getAnnotation(JReference.class);
                    String name = (jref.name().isEmpty()) ? f.getName() : jref.name();
                    Class type = f.getType();
                    Class target = jref.target();
                    if(jobject.opt(name) == JSONObject.NULL){
                        f.set(entity, null);
                    }else if(((JEntity) target.getAnnotation(JEntity.class)).subEntity()){
                        if(type.isArray()){
                            JSONArray array = jobject.getJSONArray(name);
                            Object value = Array.newInstance(target, array.length());
                            for(int i = 0; i < array.length(); i++){
                                Object e = getObject(array.getJSONObject(i), target.newInstance());
                                Array.set(value, i, e);
                            }
                            f.set(entity, value);
                        }else if(type == List.class || List.class.isAssignableFrom(type)){
                            JSONArray array = jobject.getJSONArray(name);
                            List value;
                            if(type == List.class)
                                value = new ArrayList();
                            else
                                value = (List) type.newInstance();
                            for(int i = 0; i < array.length(); i++){
                                Object e = getObject(array.getJSONObject(i), target.newInstance());
                                value.add(e);
                            }
                            f.set(entity, value);
                        }else if(type.isAnnotationPresent(JEntity.class)){
                            JSONObject obj = jobject.getJSONObject(name);
                            Object value = getObject(obj, target.newInstance());
                            f.set(entity, value);
                        }
                    }
                }else if(f.isAnnotationPresent(JKey.class)){
                    f.setInt(entity, jobject.getInt(jkey));
                }
            }
        } catch (InstantiationException ex) {
            Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(Dal.class.getName()).log(Level.SEVERE, null, ex);
        }
            setReferences(entity);
            return entity;
    }
}
